#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Keil uVision Project (.uvprojx) to CMake Converter
This script parses a Keil uVision project file and generates CMake configuration files.
"""

from io import TextIOWrapper
import xml.etree.ElementTree as ET
import os
import sys
import re
import shutil
import argparse
import json
from pathlib import Path
from typing import List, Dict, Optional, Set
from get_keil_mdk_info import get_keil_mdk_info


class KeilProjectToCMake:
    """解析Keil uVision项目文件的类"""

    CPU_INFO = re.compile(r"^(\S+)\((\S+)\)$")  # 用于解析CPU信息

    MAP_WARING = {
        "2": "-Wall -Wextra -Wno-packed -Wno-reserved-id-macro -Wno-unused-macros -Wno-documentation-unknown-command -Wno-documentation -Wno-license-management -Wno-parentheses-equality -Wno-reserved-identifier",
        "3": "-Wno-packed -Wno-missing-variable-declarations -Wno-missing-prototypes -Wno-missing-noreturn -Wno-sign-conversion -Wno-nonportable-include-path -Wno-reserved-id-macro -Wno-unused-macros -Wno-documentation-unknown-command -Wno-documentation -Wno-license-management -Wno-parentheses-equality -Wno-reserved-identifier",
        "4": "-Wno-packed -Wno-missing-variable-declarations -Wno-missing-prototypes -Wno-missing-noreturn -Wno-sign-conversion -Wno-nonportable-include-path -Wno-reserved-id-macro -Wno-unused-macros -Wno-documentation-unknown-command -Wno-documentation -Wno-license-management -Wno-parentheses-equality -Wno-reserved-identifier -Wno-covered-switch-default -Wno-unreachable-code-break",
    }

    MAP_C_STD = {
        "0": "-xc",  # <default>
        "1": "-xc -std=c90",  # c90
        "2": "-xc -std=gnu90",  # gnu90
        "3": "-xc -std=c99",  # c99
        "4": "-xc -std=gnu99",  # gnu99
        "5": "-xc -std=c11",  # c11
        "6": "-xc -std=gnu11",  # gnu11
    }

    MAP_CPP_STD = {
        "0": "-xc++ -std=gnu++14",  # <default>
        "1": "-xc++ -std=c++98",  # c++98
        "2": "-xc++ -std=gnu++98",  # gnu++98
        "3": "-xc++ -std=c++11",  # c++11
        "4": "-xc++ -std=gnu++11",  # gnu++11
        "5": "-xc++ -std=c++03",  # c++03
        "6": "-xc++ -std=c++14",  # c++14
        "7": "-xc++ -std=gnu++14",  # gnu++14
        "8": "-xc++ -std=c++17",  # -std=c++17
        "9": "-xc++ -std=gnu++17",  # -std=gnu++17
    }

    MAP_ASM_STD = {
        "0": "-masm=auto -Wa,armasm,--diag_suppress=A1950W",  # armclang(Auto)
        "2": "-masm=gnu",  # armclang(GNU Syntax)
        "3": "-masm=armasm -Wa,armasm,--diag_suppress=A1950W",  # armclang(Arm Syntax)
    }

    MAP_OPTIM = {
        "0": "",  # <default>
        "1": "-O0",
        "2": "-O1",
        "3": "-O2",
        "4": "-O3",
        "5": "-Ofast",
        "6": "-Os",
        "7": "-Oz",
        "8": "-Omax",
    }

    def __init__(
        self, uvprojx_path: str, uv4_path: Optional[str] = None, verbose: bool = False
    ):
        if uv4_path is None:
            # 默认路径
            default_path = str(Path.home() / "AppData" / "Local" / "Keil_v5")
            
            # 检查默认路径是否存在
            if os.path.exists(default_path):
                self.uv4_path = default_path
            else:
                # 搜索C盘根目录下的Keil_v5
                c_drive_keil_path = "C:\\Keil_v5"
                if os.path.exists(c_drive_keil_path):
                    self.uv4_path = c_drive_keil_path
                else:
                    # 使用默认路径（即使不存在，后续会有错误提示）
                    self.uv4_path = default_path
                    if verbose:
                        print(f"警告: 默认路径不存在: {default_path}")
                        print(f"警告: C盘路径不存在: {c_drive_keil_path}")
        else:
            self.uv4_path = uv4_path

        self.mdk_info: Optional[Dict[str, str]] = get_keil_mdk_info(self.uv4_path)

        self.uvprojx_path = Path(uvprojx_path)
        self.project_dir = self.uvprojx_path.parent
        self.project_name = self.uvprojx_path.stem
        self.verbose = verbose

        # 项目信息
        self.output_directory = ""
        self.output_name = ""
        self.target_name = ""
        self.device_name = ""
        self.vendor = ""
        self.cpu = ""

        # 源文件和头文件
        self.source_files: List[str] = []
        self.header_files: List[str] = []
        self.include_paths: Set[str] = set()

        # 编译器设置
        self.defines: Set[str] = set()
        self.cpu_flags: List[str] = ["--target=arm-arm-none-eabi"]
        self.c_cpp_flags: List[str] = []  # C/Cpp 编译标记
        self.c_flags: List[str] = []  # C 编译标记
        self.cpp_flags: List[str] = []  # C++ 编译标记
        self.asm_flags: List[str] = []  # 汇编编译标记
        self.linker_flags: List[str] = []
        self.linker_script = ""

        # 库文件
        self.libraries: List[str] = []
        self.library_paths: Set[str] = set()

    def parse(self) -> bool:
        """解析uVision项目文件"""

        if self.mdk_info is None:
            print(
                f"错误: 无法获取 Keil MDK 信息，请检查路径: {self.uv4_path}",
                file=sys.stderr,
            )
            return False

        try:
            if not self.uvprojx_path.exists():
                print(f"错误: 项目文件不存在 {self.uvprojx_path}", file=sys.stderr)
                return False

            # 解析XML文件
            tree = ET.parse(self.uvprojx_path)
            root = tree.getroot()

            targets = root.findall(".//Target")

            if len(targets) == 0:
                return False

            # 解析项目基本信息
            self._parse_project_info(targets[0])

            # 解析目标设备信息
            self._parse_target_info(targets[0])

            # 解析源文件
            self._parse_source_files(targets[0])

            # 解析编译器设置
            self._parse_compiler_settings(targets[0])

            # 解析链接器设置
            self._parse_linker_settings(targets[0])

            self._parse_runtime_env(root, self.target_name)

            return True

        except ET.ParseError as e:
            print(f"XML解析错误: {e}", file=sys.stderr)
            return False
        except Exception as e:
            print(f"解析项目文件时发生错误: {e}", file=sys.stderr)
            return False

    def _parse_project_info(self, root: ET.Element):
        """解析项目基本信息"""
        # 获取项目名称
        target_name = self._get_element_text(root, ".//TargetName")
        self.target_name = target_name if target_name else self.project_name

        # 获取输出目录
        self.output_directory = self._get_element_text(
            root, ".//OutputDirectory", "Objects"
        )
        if not self.output_directory:
            self.output_directory = "Objects"

        # 获取输出名称
        self.output_name = self._get_element_text(root, ".//OutputName")
        if not self.output_name:
            self.output_name = self.target_name

    def _parse_target_info(self, root: ET.Element):
        """解析目标设备信息"""
        # 设备名称
        self.device_name = self._get_element_text(root, ".//Device")

        # 厂商信息
        self.vendor = self._get_element_text(root, ".//Vendor")

        # CPU类型
        self.cpu_type = self._get_element_text(root, ".//Cpu")

        if self.cpu_type == "":
            return

    def _parse_source_files(self, root: ET.Element):
        """解析源文件和头文件"""
        # 查找所有文件组
        groups = root.findall(".//Group")

        for group in groups:
            files = group.findall(".//File")
            for file_node in files:
                file_name = self._get_element_text(file_node, "FileName")
                file_path = self._get_element_text(file_node, "FilePath")
                file_type = self._get_element_text(file_node, "FileType")

                if file_name and file_path:
                    # 转换路径分隔符并解析相对路径
                    file_path = file_path.replace("\\", "/")
                    if not os.path.isabs(file_path):
                        file_path = os.path.join(self.project_dir, file_path)

                    file_path = os.path.normpath(file_path)

                    # 根据文件类型分类
                    file_ext = os.path.splitext(file_path)[1].lower()

                    if file_ext in [".c", ".cpp", ".cc", ".cxx", ".s", ".asm"]:
                        self.source_files.append(file_path)
                    elif file_ext in [".h", ".hpp", ".hxx"]:
                        self.header_files.append(file_path)
                        # 添加头文件目录到包含路径
                        include_dir = os.path.dirname(file_path)
                        if include_dir:
                            self.include_paths.add(include_dir)

    def _get_element_text(
        self, parent: ET.Element, xpath: str, default: str = ""
    ) -> str:
        """获取XML元素的文本内容

        Args:
            parent: 父元素
            xpath: XPath表达式
            default: 默认值

        Returns:
            str: 元素的文本内容，如果不存在则返回默认值
        """
        node = parent.find(xpath)
        if node is not None and node.text:
            return node.text
        return default

    def _parse_flags(
        self, parent: ET.Element, child_name: str, options: dict[str, str]
    ) -> str:
        """解析编译标志

        Args:
            parent: 父元素
            child_name: 子元素名称
            options: 选项映射字典

        Returns:
            str: 解析得到的编译标志，如果没有则返回空字符串
        """
        txt = self._get_element_text(parent, f".//{child_name}")

        if txt in options:
            return options[txt] if options[txt] else ""
        else:
            return options.get("default", "")

    def _parse_compiler_settings(self, root: ET.Element):
        """解析编译器设置"""

        # 解析CPU特性
        cpu_feature = {}
        for item in self.cpu_type.split(" "):
            m = self.CPU_INFO.match(item)
            if m is None:
                match (item):
                    case "DSP":
                        cpu_feature["DSP"] = 1
                    case "PACBTI":
                        cpu_feature["PACBTI"] = 1
                    case "ELITTLE":
                        cpu_feature["ENDIAN"] = "little"
                    case "FPU2":
                        cpu_feature["FPU2"] = 1
                    case _:
                        if self.verbose:
                            print(f"未处理的信息: {item}")
            else:
                key = m.group(1)
                value = m.group(2).replace('"', "")

                match key:
                    case "CPUTYPE":
                        self.cpu = value
                    case "MVE":
                        cpu_feature["MVE"] = value
                    case "CDECP":
                        cpu_feature["CDE"] = value
                    case "FPU3":
                        cpu_feature["FPU"] = value
                    case "FPU":
                        cpu_feature["FPU"] = value

                    case _:
                        if self.verbose:
                            print(f"未知特性: {key} = {value}")

        cpu = self.cpu.lower()
        features = [f"-mcpu={cpu}"]

        # FPU
        fpu_txt = self._get_element_text(root, ".//RvdsVP")

        if fpu_txt == "2" or fpu_txt == "3":
            self.cpu_flags += ["-mfloat-abi=hard"]
        else:
            self.cpu_flags += ["-mfloat-abi=soft"]

        match (cpu):
            case "cortex-m52" | "cortex-m55" | "cortex-m85":
                # MVE
                mve_txt = self._get_element_text(root, ".//RvdsMve")
                if mve_txt:
                    if self.verbose:
                        print(f"MVE类型: {mve_txt}")

                # FPU
                # 重新获取FPU类型用于后续处理
                fpu_txt = self._get_element_text(root, ".//RvdsVP")
                if fpu_txt:
                    if self.verbose:
                        print(f"FVP类型: {fpu_txt}")

                if (
                    fpu_txt == "1"
                    and mve_txt == "0"
                    or fpu_txt == "3"
                    and mve_txt == "1"
                    or fpu_txt == "2"
                    and mve_txt == "2"
                ):
                    raise ValueError(f"No support: FPU={fpu_txt}, MVE={mve_txt}.")

                if mve_txt == "2":
                    # MVE FP half-precision and single-precision
                    pass
                elif mve_txt == "1":
                    # MVE integer only
                    features += ["+nomve.fp"]
                else:
                    # No MVE
                    features += ["+nomve"]

                if fpu_txt == "3":
                    # Scalar FP double-precision and single-precision and half-precision
                    pass
                elif fpu_txt == "2":
                    # Scalar single-precision and half-precision
                    features += ["+nofp.dp"]
                else:
                    # No FPU
                    features += ["+nofp"]

                # PACBTI
                if "PACBTI" not in cpu_feature:
                    features += ["+nopacbti"]
                else:
                    features += ["+pacbti"]

                cde_txt = self._get_element_text(root, ".//RvdsCdeCp")
                if cde_txt:
                    number = int(cde_txt)
                    for i in range(8):
                        if number & (1 << i):
                            features += [f"+cdecp{i}"]
            case "cortex-m4":
                if fpu_txt == "2":
                    self.cpu_flags += ["-mfpu=fpv4-sp-d16"]
                else:
                    self.cpu_flags += ["-mfpu=none"]
            case "cortex-m33" | "cortex-m35p":
                if fpu_txt == "2":
                    self.cpu_flags += ["-mfpu=fpv5-sp-d16"]
                else:
                    self.cpu_flags += ["-mfpu=none"]
            case "cortex-m7":
                if fpu_txt == "2":
                    self.cpu_flags += ["-mfpu=fpv5-sp-d16"]
                elif fpu_txt == "3":
                    self.cpu_flags += ["-mfpu=fpv5-d16"]
                else:
                    self.cpu_flags += ["-mfpu=none"]
            case _:
                pass

        self.cpu_flags += ["".join(features)]

        branchprot_txt = self._get_element_text(root, ".//nBranchProt")
        if branchprot_txt == "2":
            self.cpu_flags += ["-mbranch-protection=bti+pac-ret"]
        elif branchprot_txt == "1":
            self.cpu_flags += ["-mbranch-protection=bti"]

        if "ENDIAN" in cpu_feature and cpu_feature["ENDIAN"] == "little":
            self.cpu_flags += ["-mlittle-endian"]
        else:
            self.cpu_flags += ["-mbig-endian"]

        # C编译器设置
        self.c_cpp_flags += ["-ffunction-sections"]

        c_compiler = root.find(".//Cads")
        if c_compiler is not None:
            # 预定义宏
            defines_text = self._get_element_text(
                c_compiler, ".//VariousControls/Define"
            )
            if defines_text:
                defines = defines_text.replace(" ", "").split(",")
                for define in defines:
                    if define.strip():
                        self.defines.add(define.strip())

            # 包含路径
            include_text = self._get_element_text(
                c_compiler, ".//VariousControls/IncludePath"
            )
            if include_text:
                includes = include_text.split(";")
                for include in includes:
                    include = include.strip().replace("\\", "/")
                    if include and include != ".":
                        if not os.path.isabs(include):
                            include = os.path.join(self.project_dir, include)
                        include = os.path.normpath(include)
                        self.include_paths.add(include)

            #
            flag = self._parse_flags(
                c_compiler, "Optim", self.MAP_OPTIM
            )  # <TODO> -flto
            if flag:
                self.c_cpp_flags.append(flag)

            flag = self._parse_flags(
                c_compiler, "v6Rtti", {"1": "", "default": "-fno-rtti"}
            )
            if flag:
                self.c_cpp_flags.append(flag)
            flag = self._parse_flags(
                c_compiler,
                "PlainCh",
                {"1": "-fsigned-char", "default": "-funsigned-char"},
            )
            if flag:
                self.c_cpp_flags.append(flag)
            flag = self._parse_flags(c_compiler, "vShortEn", {"1": "-fshort-enums"})
            if flag:
                self.c_cpp_flags.append(flag)
            flag = self._parse_flags(c_compiler, "vShortWch", {"1": "-fshort-wchar"})
            if flag:
                self.c_cpp_flags.append(flag)
            flag = self._parse_flags(c_compiler, "v6Lto", {"1": "-flto"})
            if flag:
                self.c_cpp_flags.append(flag)
            flag = self._parse_flags(c_compiler, "SplitLS", {"1": "-fno-ldm-stm"})
            if flag:
                self.c_cpp_flags.append(flag)
            flag = self._parse_flags(c_compiler, "Ropi", {"1": "-fropi"})
            if flag:
                self.c_cpp_flags.append(flag)
            flag = self._parse_flags(c_compiler, "Rwpi", {"1": "-frwpi"})
            if flag:
                self.c_cpp_flags.append(flag)
            flag = self._parse_flags(c_compiler, "wLevel", self.MAP_WARING)
            if flag:
                self.c_cpp_flags.append(flag)
            flag = self._parse_flags(c_compiler, "v6WtE", {"1": "-Werror"})
            if flag:
                self.c_cpp_flags.append(flag)

            # 其他编译器选项
            misc_text = self._get_element_text(
                c_compiler, ".//VariousControls/MiscControls"
            )
            if misc_text:
                self.c_cpp_flags.extend(misc_text.split())

            # C FLAGS
            flag = self._parse_flags(c_compiler, "v6Lang", self.MAP_C_STD)
            if flag:
                self.c_flags.append(flag)

            # CPP FLAGS
            flag = self._parse_flags(c_compiler, "v6LangP", self.MAP_CPP_STD)
            if flag:
                self.cpp_flags.append(flag)

        # ASM Flags
        flag = self._parse_flags(root, "ClangAsOpt", self.MAP_ASM_STD)
        if flag:
            self.asm_flags.append(flag)
        else:
            self.asm_flags.append(self.MAP_ASM_STD["0"])

        # C++编译器设置
        cpp_compiler = root.find(".//Cppads")
        if cpp_compiler is not None:
            # C++特定的设置可以在这里添加
            pass

    def _parse_linker_settings(self, root: ET.Element):
        """解析链接器设置"""
        linker = root.find(".//LDads")
        if linker is not None:
            # 链接器脚本
            scatter_text = self._get_element_text(linker, ".//ScatterFile")
            if scatter_text:
                linker_script = scatter_text.replace("\\", "/")
                if not os.path.isabs(linker_script):
                    linker_script = os.path.join(self.project_dir, linker_script)
                self.linker_script = os.path.normpath(linker_script)

            # 其他链接器选项
            misc_text = self._get_element_text(linker, ".//Misc")
            if misc_text:
                self.linker_flags.extend(misc_text.split())

        self.linker_flags.extend(
            [
                "--summary_stderr",
                "--info summarysizes",
                "--map",
                "--load_addr_map_info",
                "--xref",
                "--callgraph",
                "--symbols",
                "--info sizes",
                "--info totals",
                "--info unused",
                "--info veneers",
            ]
        )

    def _search_pdsc_file(
        self,
        filepath: str,
        cls: Optional[str],
        condition: Optional[str],
        sub: Optional[str] = None,
    ) -> None:
        """查找PDSC文件中指定类别和子类别的文件"""
        if not os.path.exists(filepath):
            return

        base = os.path.dirname(filepath)
        tree = ET.parse(filepath)
        root = tree.getroot()

        for c_node in root.findall(".//component"):
            if (
                c_node.get("Cclass") == cls
                and c_node.get("Csub") == sub
                and condition == c_node.get("condition")
            ):

                for file_node in c_node.findall(".//file"):
                    if file_node is None:
                        continue

                    if file_node.get("attr") is not None:
                        # 如果有attr属性，表示这是一个特殊的文件，不处理
                        continue

                    c = file_node.get("category")
                    p = file_node.get("name")

                    if c is None or p is None:
                        continue
                    p = os.path.join(base, p).replace("\\", "/")

                    if c == "source":
                        self.source_files.append(p)
                    else:
                        if os.path.isdir(p):
                            self.include_paths.add(p)
                        #
                    #
                #
            #
        # end-for

    def _parse_runtime_env(self, root: ET.Element, target_name: str):
        """解析运行时环境设置"""
        runtime_env = root.find(".//RTE")
        if runtime_env is None:
            return

        for c_node in runtime_env.findall(".//component"):
            targets = [n.get("name") for n in c_node.findall(".//targetInfo")]
            if target_name not in targets:
                continue

            p_node = c_node.find(".//package")
            package_name = p_node.get("name") if p_node is not None else ""
            package_vendor = p_node.get("vendor") if p_node is not None else ""
            package_version = p_node.get("version") if p_node is not None else ""

            package_direpath = Path(self.mdk_info["RTEPATH"]) / Path(package_vendor) / Path(package_name) / Path(package_version)  # type: ignore

            package_filepath = (
                package_direpath / f"{package_vendor}.{package_name}.pdsc"
            )

            c_class = c_node.get("Cclass")
            c_group = c_node.get("Cgroup")
            c_sub = c_node.get("Csub")
            c_vendor = c_node.get("Cvendor")
            condition = c_node.get("condition")

            self._search_pdsc_file(str(package_filepath), c_class, condition, c_sub)

        for f_node in runtime_env.findall(".//file"):

            targets = [n.get("name") for n in f_node.findall(".//targetInfo")]
            if target_name not in targets:
                continue

            name = f_node.get("name")
            if name is None:
                continue

            package = f_node.find(".//package")
            if package is not None:
                package_name = package.get("name")
                package_vendor = package.get("vendor")
                package_version = package.get("version")

                if package_name and package_vendor and package_version:
                    p = Path(self.mdk_info["RTEPATH"]) / Path(package_vendor) / Path(package_name) / Path(package_version) / Path(name)  # type: ignore

                    self.source_files.append(str(p))

    def generate_cmake(
        self, output_dir: Optional[str] = None, export_vsc_settings: bool = False
    ) -> bool:
        """生成 CMake 文件"""
        if output_dir is None:
            output_path = self.project_dir
        else:
            output_path = Path(output_dir)

        output_path.mkdir(parents=True, exist_ok=True)
        cmake_file = output_path / "CMakeLists.txt"
        rc = False

        try:
            with open(cmake_file, "w", encoding="utf-8") as f:
                self._write_cmake_content(f, str(output_path))

            if self.verbose:
                print(f"\nCMake文件已生成: {cmake_file}")
            # 复制必要的资源文件
            my_dir = os.path.dirname(os.path.abspath(__file__))
            cmake_dir = os.path.join(output_path, "cmake")
            os.makedirs(cmake_dir, exist_ok=True)

            shutil.copy(
                os.path.join(my_dir, "asset/do_build.bat"),
                os.path.join(output_path, "do_build.bat"),
            )

            shutil.copy(
                os.path.join(my_dir, "asset/cmake/armclang.cmake"),
                os.path.join(cmake_dir, "armclang.cmake"),
            )

            if export_vsc_settings:
                # 生成/更新 VSCode 设置文件
                vsc_settings = output_path / ".vscode"
                vsc_settings.mkdir(exist_ok=True)

                # settings.json, 主要是 cmake-tools 插件的配置
                settings = {
                    "cmake.generator": "Ninja",
                    "cmake.configureEnvironment": {
                        "ARMCLANG_PATH": os.path.join(
                            self.uv4_path, "ARM\\ARMCLANG\\bin"
                        ).replace("\\", "/"),
                    },
                }
                settings_file = vsc_settings / "settings.json"
                json.dump(
                    settings, open(settings_file, "w", encoding="utf-8"), indent=4
                )

                # launch.json 调试用~暂时不支持
                launch = {
                    "version": "0.2.0",
                    "configurations": [
                        {
                            "cwd": "${workspaceFolder}",
                            "executable": f"build/{self.target_name}.axf",
                            "name": "Debug with PyOCD",
                            "request": "launch",
                            "type": "cortex-debug",
                            "runToEntryPoint": "main",
                            "showDevDebugOutput": "none",
                            "servertype": "pyocd",
                        },
                        {
                            "name": "Cortex Debug",
                            "cwd": "${workspaceFolder}",
                            "executable": f"build/{self.target_name}.axf",
                            "request": "launch",
                            "type": "cortex-debug",
                            "runToEntryPoint": "main",
                            "servertype": "jlink",
                        },
                    ],
                }

                settings_file = vsc_settings / "launch.json"
                json.dump(launch, open(settings_file, "w", encoding="utf-8"), indent=4)

                c_cpp_properties = {
                    "configurations": [
                        {
                            "name": "Win32",
                            "compileCommands": "${workspaceFolder}/build/compile_commands.json",
                            "configurationProvider": "ms-vscode.cmake-tools",
                        }
                    ],
                    "version": 4,
                }

                settings_file = vsc_settings / "c_cpp_properties.json"
                json.dump(
                    c_cpp_properties,
                    open(settings_file, "w", encoding="utf-8"),
                    indent=4,
                )

                tasks = {
                    "version": "2.0.0",
                    "tasks": [
                        {
                            "label": "Build",
                            "type": "shell",
                            "command": "cmake",
                            "args": ["--build", "${workspaceFolder}/build"],
                            "group": {"kind": "build", "isDefault": True},
                            "problemMatcher": [],
                        },
                        {
                            "label": "Rebuild",
                            "type": "shell",
                            "command": "${workspaceFolder}\\do_build.bat",
                            "problemMatcher": [],
                        },
                        {
                            "label": "Flash (Keil MDK)",
                            "type": "shell",
                            "command": os.path.join(self.uv4_path, "UV4\\UV4.exe"),
                            "args": [
                                "-f",
                                str(self.uvprojx_path),
                            ],
                            "problemMatcher": [],
                        },
                        {
                            "label": "Debug (Keil MDK)",
                            "type": "shell",
                            "command": os.path.join(self.uv4_path, "UV4\\UV4.exe"),
                            "args": [
                                "-d",
                                str(self.uvprojx_path),
                            ],
                            "problemMatcher": [],
                        },
                    ],
                }

                settings_file = vsc_settings / "tasks.json"
                json.dump(tasks, open(settings_file, "w", encoding="utf-8"), indent=4)

                if self.verbose:
                    print(f"\nVS Code 设置已创建/更新.")

            rc = True

        except Exception as e:
            print(f"生成CMake文件时发生错误: {e}", file=sys.stderr)
            rc = False

        return rc

    def _normalize_path(self, path: str, dest: str) -> str:

        try:
            rel_path = os.path.relpath(path, dest).replace("\\", "/")
        except ValueError:
            # 如果路径无法转换，直接使用绝对路径
            rel_path = path.replace("\\", "/")

        return rel_path

    def _write_cmake_content(self, f: TextIOWrapper, dest: str) -> None:
        """写入CMake文件内容"""

        f.write("# CMake file generated from Keil uVision project\n")
        f.write("# Generated by keil_uvprojx2cmake.py\n\n")

        f.write("cmake_minimum_required(VERSION 3.15)\n\n")
        f.write("cmake_policy(SET CMP0123 NEW)\n\n")

        f.write("include(cmake/armclang.cmake)\n\n")

        # 项目名称
        f.write(f"project({self.target_name} C CXX ASM)\n")
        f.write("set(CMAKE_EXPORT_COMPILE_COMMANDS ON)\n")
        f.write("# set(CMAKE_VERBOSE_MAKEFILE ON)\n\n")

        # 设备信息注释
        if self.device_name:
            f.write(f"# Target Device: {self.device_name}\n")
        if self.vendor:
            f.write(f"# Vendor: {self.vendor}\n")
        if self.cpu_type:
            f.write(f"# CPU: {self.cpu_type}\n")
        f.write("\n")

        # 编译器标志
        self.cpu_flags += ["-gdwarf-4"]
        comm_flags = " ".join(self.cpu_flags)
        f.write("# Compiler flags\n")
        f.write(f'set(CPU_FLAGS "{comm_flags}")\n')
        f.write(
            f'set(CMAKE_C_FLAGS "${{CPU_FLAGS}} {" ".join(self.c_flags)} {" ".join(self.c_cpp_flags)}")\n'
        )
        f.write(
            f'set(CMAKE_CXX_FLAGS "${{CPU_FLAGS}} {" ".join(self.cpp_flags)} {" ".join(self.c_cpp_flags)}")\n'
        )
        f.write(f'set(CMAKE_ASM_FLAGS "${{CPU_FLAGS}} {" ".join(self.asm_flags)}")\n\n')

        # 链接器标志
        if self.linker_flags:
            f.write("# Linker flags\n")
            flags_str = " ".join(self.linker_flags)
            f.write(
                f'set(CMAKE_EXE_LINKER_FLAGS "${{CMAKE_EXE_LINKER_FLAGS}} {flags_str}")\n\n'
            )

        # 链接器脚本
        if self.linker_script:
            p = self.project_dir / self.linker_script
            rel_linker_script = self._normalize_path(str(p), dest)

            f.write(
                f'set(LINKER_SCRIPT "${{CMAKE_CURRENT_SOURCE_DIR}}/{rel_linker_script}")\n'
            )
            f.write(
                'set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --strict --scatter \\"${LINKER_SCRIPT}\\"")\n\n'
            )

        # 预定义宏
        if self.defines:
            f.write("# Preprocessor definitions\n")
            f.write("add_definitions(\n")
            for define in sorted(self.defines):
                f.write(f"    -D{define}\n")
            f.write(")\n\n")

        # 包含路径
        if self.include_paths:
            f.write("# Include directories\n")
            f.write("include_directories(\n")
            for include_path in sorted(self.include_paths):
                rel_path = self._normalize_path(include_path, dest)
                f.write(f'    "{rel_path}"\n')
            f.write(")\n\n")

        # 源文件
        if self.source_files:
            f.write("# Source files\n")
            f.write("set(SOURCES\n")
            for source_file in sorted(self.source_files):
                rel_path = self._normalize_path(source_file, dest)
                f.write(f'    "{rel_path}"\n')
            f.write(")\n\n")

        # 创建可执行文件
        f.write("# Create executable\n")
        f.write(f"add_executable({self.target_name} ${{SOURCES}})\n\n")
        f.write(
            f'set_target_properties({self.target_name} PROPERTIES SUFFIX ".axf")\n\n'
        )

        # 库文件（如果有）
        if self.libraries:
            f.write("# Link libraries\n")
            f.write(f"target_link_libraries({self.target_name}\n")
            for lib in self.libraries:
                f.write(f"    {lib}\n")
            f.write(")\n\n")

        # 生成的 axf 文件拷贝到 keil 工程目录
        output_directory = os.path.join(self.project_dir, self.output_directory)
        output_directory = os.path.normpath(output_directory).replace("\\", "/")
        f.write(f'set(DEST_DIR "{output_directory}")\n')
        f.write(
            f"""add_custom_target(always_copy ALL
    COMMAND ${{CMAKE_COMMAND}} -E make_directory "${{DEST_DIR}}"
    COMMAND ${{CMAKE_COMMAND}} -E copy_if_different "$<TARGET_FILE:{self.target_name}>" "${{DEST_DIR}}/{self.output_name}.axf"
    DEPENDS {self.target_name}
)\n"""
        )

        f.write(f'set_property(TARGET always_copy PROPERTY FOLDER "postbuild")\n')

    def print_project_info(self):
        """打印项目信息"""
        print(f"项目名称: {self.target_name}")
        print(f"设备: {self.device_name}")
        print(f"厂商: {self.vendor}")
        print(f"CPU: {self.cpu}")
        print(f"编译器标志: {self.c_cpp_flags}, {self.linker_flags}")
        print(f"源文件数量: {len(self.source_files)}")
        print(f"头文件数量: {len(self.header_files)}")
        print(f"包含路径数量: {len(self.include_paths)}")
        print(f"预定义宏数量: {len(self.defines)}")

        if self.source_files:
            print("\n源文件:")
            for src in self.source_files[:10]:  # 只显示前10个
                print(f"  {src}")
            if len(self.source_files) > 10:
                print(f"  ... 还有 {len(self.source_files) - 10} 个文件")

        if self.defines:
            print(f"\n预定义宏: {', '.join(sorted(list(self.defines)[:10]))}")
            if len(self.defines) > 10:
                print(f"  ... 还有 {len(self.defines) - 10} 个宏")


def main(
    filepath: str,
    output_dir: Optional[str],
    uv4_path: Optional[str],
    verbose: bool,
    export_vsc_settings: bool,
):
    """主函数，解析Keil项目文件并生成CMake文件
    @param filepath: Keil项目文件路径
    @param output_dir: 输出目录（可选）
    @param uv4_path: Keil UV4 安装路径（可选）
    @param verbose: 是否启用详细模式
    @param export_vsc_settings: 是否启用导出模式（生成VSCode设置）
    """
    if verbose:
        print(f"Keil 项目文件: {filepath}")

    # 创建解析器实例
    parser = KeilProjectToCMake(filepath, uv4_path, verbose)

    # 解析项目文件
    if not parser.parse():
        print("解析失败!", file=sys.stderr)
        sys.exit(1)

    # 打印项目信息
    if verbose:
        parser.print_project_info()

    # 生成CMake文件
    if not parser.generate_cmake(output_dir, export_vsc_settings):
        print("生成 CMake 文件失败!", file=sys.stderr)
        sys.exit(1)

    # end


if __name__ == "__main__":
    """主函数"""
    parser = argparse.ArgumentParser(description="解析 Keil 项目文件")
    parser.add_argument("path", help="Keil 项目文件路径")
    parser.add_argument("-v", "--verbose", action="store_true", help="启用详细模式")
    parser.add_argument(
        "-e", "--export-vsc-settings", action="store_true", help="导出 VS Code 设置"
    )
    parser.add_argument(
        "-d",
        "--destination",
        dest="dest",
        help="输出目录（可选，默认为项目文件所在目录）",
    )
    parser.add_argument(
        "-a",
        "--ask-dest",
        dest="ask_dest",
        action="store_true",
        default=False,
        help="弹出对话框选择输出目录，-d 参数不再生效",
    )
    parser.add_argument(
        "--uv4-path",
        dest="uv4_path",
        help="Keil UV4 安装路径（可选）",
    )
    args = parser.parse_args()

    if args.ask_dest:
        from tkinter import filedialog, messagebox
        import tkinter as tk

        # 创建一个隐藏的根窗口
        root = tk.Tk()
        root.withdraw()  # 隐藏主窗口

        # 1. askquestion - 返回 'yes' 或 'no' 字符串
        rc = messagebox.askquestion(
            "请确认", "输出目录是当前工程目录吗？\n选择【否】将打开文件夹选择对话框"
        )
        if rc == "no":
            rc = ""
            # 打开文件夹选择对话框
            rc = filedialog.askdirectory(
                title="请选择文件夹", initialdir=os.getcwd()  # 初始目录为当前工作目录
            )
        else:
            rc = os.path.dirname(args.filepath)  # 使用项目文件所在目录

        # 销毁根窗口
        root.destroy()

        if rc == "":
            print("未选择输出目录，程序退出。", file=sys.stderr)
            sys.exit(1)
        dest = rc
    else:
        dest = args.dest

    main(args.path, dest, args.uv4_path, args.verbose, args.export_vsc_settings)
